package com.ayit.klog;


import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.util.Log;

import android.util.Pair;
import com.ayit.klog.klog.BaseLog;
import com.ayit.klog.klog.FileLog;
import com.ayit.klog.klog.JsonLog;
import com.ayit.klog.klog.XmlLog;
import com.ayit.klog.utils.ThrowableUtils;
import com.ayit.klog.utils.ZipUtils;

import java.io.File;
import java.io.FileFilter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * This is a Log tool，with this you can the following
 * <ol>
 * <li>use KLog.d(),you could print whether the method execute,and the default tag is current class's name</li>
 * <li>use KLog.d(msg),you could print log as before,and you could location the method with a click in Android Studio Logcat</li>
 * <li>use KLog.json(),you could print json string with well format automatic</li>
 * </ol>
 *
 * @author zhaokaiqiang github https://github.com/ZhaoKaiQiang/KLog 15/11/17 扩展功能，添加对文件的支持 15/11/18
 * 扩展功能，增加对XML的支持，修复BUG 15/12/8  扩展功能，添加对任意参数的支持 15/12/11 扩展功能，增加对无限长字符串支持 16/6/13
 * 扩展功能，添加对自定义全局Tag的支持,修复内部类不能点击跳转的BUG 16/6/15  扩展功能，添加不能关闭的KLog.debug(),用于发布版本的Log打印,优化部分代码 16/6/20
 * 扩展功能，添加堆栈跟踪功能KLog.trace()
 */
public class KLog {
    
    public static AutoFileWriter fileWriter;
    
    public static DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA);
    
    public static DateFormat dataFormatter = new SimpleDateFormat("yyyy-MM-dd", Locale.CHINA);
    
    private static long lastCheckFileTime = 0;
    
    
    /**
     * 将log 写入到文件中
     *
     * @param msg
     */
    public static void writeFile(boolean isRealTime, String msg) {
        if (!IS_WRITE_LOG) {
            return;
        }
        writeMsg(isRealTime, msg);
    }
    
    private static void writeMsg(String msg) {
        writeMsg(false, msg);
    }
    
    private static void writeMsg(boolean isRealTime, String msg) {
        if (writeThread == null) {
            return;
        }
        if (isRealTime) {
            writeRealMsg2File(getCurrentTime(), msg);
            return;
        }
        Object[] arr = new Object[2];
        arr[0] = getCurrentTime();
        arr[1] = msg;
        writeMsgQueue.offer(arr);
    }
    
    public static final String LINE_SEPARATOR = System.getProperty("line.separator");
    
    public static final String NULL_TIPS = "Log with null object";
    
    private static final String DEFAULT_MESSAGE = "execute";
    
    private static final String PARAM = "Param";
    
    private static final String NULL = "null";
    
    private static final String TAG_DEFAULT = "klog";
    
    private static final long LOGFILES_HOLDING_DAYS_DEFAULT = 7;
    
    private static final long LOGFILES_HOLDING_SIZE_DEFAULT = 1024 * 1024 * 500;
    
    private static final String SUFFIX = ".java";
    
    public static final int JSON_INDENT = 4;
    
    public static final int V = 0x1;
    
    public static final int D = 0x2;
    
    public static final int I = 0x3;
    
    public static final int W = 0x4;
    
    public static final int E = 0x5;
    
    public static final int A = 0x6;
    
    private static final int JSON = 0x7;
    
    private static final int XML = 0x8;
    
    private static final int STACK_TRACE_INDEX_5 = 5;
    
    private static final int STACK_TRACE_INDEX_4 = 4;
    
    private static String mGlobalTag;
    
    private static boolean mIsGlobalTagEmpty = true;
    
    public static boolean IS_SHOW_LOG = true;
    
    public static boolean IS_WRITE_LOG = false;
    
    private static String mGlobalLogFilesPercent;
    
    private static long mGlobalLogFilesSize = 1024 * 1024 * 500;
    
    private static long mGlobalHoldingDays;
    
    public static String currentLogDay;
    
    private static Thread writeThread;
    
    private static LinkedBlockingQueue<Object[]> writeMsgQueue = new LinkedBlockingQueue<>();
    
    private static Handler zipHandler;
    
    public static void init(Context context, boolean isShowLog) {
        String path = context.getExternalCacheDir() + "/klogs/";
        init(context, isShowLog, TAG_DEFAULT);
    }
    
    public static void init(Context context, boolean isShowLog, @Nullable String tag) {
        String path = context.getExternalCacheDir() + "/klogs/";
        init(context, isShowLog, false, tag, path, LOGFILES_HOLDING_DAYS_DEFAULT,
                LOGFILES_HOLDING_SIZE_DEFAULT);
    }
    
    public static void init(Context context, boolean isShowLog, boolean isWriteLog,
            @Nullable String tag, String logFilesDir, long holdingDays) {
        init(context, isShowLog, false, tag, logFilesDir, LOGFILES_HOLDING_DAYS_DEFAULT,
                holdingDays);
    }
    
    public static void init(Context context, boolean isShowLog, boolean isWriteLog,
            @Nullable String tag, String logFilesDir, long holdingDays, long logFilesSize) {
        IS_SHOW_LOG = isShowLog;
        IS_WRITE_LOG = isWriteLog;
        mGlobalTag = tag;
        mIsGlobalTagEmpty = TextUtils.isEmpty(mGlobalTag);
        mGlobalLogFilesPercent = logFilesDir;
        mGlobalHoldingDays = holdingDays;
        mGlobalLogFilesSize = logFilesSize;
        Log.d(tag, logFilesDir);
        writeThread = new Thread(new Runnable() {
            StringBuilder msgBuf = new StringBuilder();
            
            long lastWriteTime = 0;
            
            @Override
            public void run() {
                try {
                    long msgTime = getCurrentTime();
                    while (true) {
                        Object[] msgArr = writeMsgQueue.poll(100, TimeUnit.MILLISECONDS);
                        if (null != msgArr) {
                            String msg = (String) msgArr[1];
                            msgTime = (long) msgArr[0];
                            if (!TextUtils.isEmpty(msg)) {
                                msgBuf.append(msg).append(KLog.LINE_SEPARATOR);
                            }
                        }
                        if (msgBuf.length() > 0 && (msgBuf.length() > 10 * 1024
                                || getCurrentTime() - lastWriteTime > 500)) {
                            writeMsg2File(msgTime, msgBuf.toString());
                            msgBuf.setLength(0);
                            lastWriteTime = getCurrentTime();
                        }
                    }
                } catch (Throwable e) {
                    writeMsg2File(getCurrentTime(),
                            "write queue err! " + ThrowableUtils.getFullStackTrace(e));
                }
                
            }
        });
        writeThread.start();
        HandlerThread htZip = new HandlerThread("KLOG_ZIP");
        htZip.start();
        zipHandler = new Handler(htZip.getLooper()) {
            @Override
            public void handleMessage(Message msg) {
                zipFile();
            }
        };
        
    }
    
    protected static void writeMsg2File(long currentTime, String msg) {
        closeFileWriter();
        clearTimeOutFile(currentTime);
        checkLogFileSize();
        newLogFile(currentTime);
        lastCheckFileTime = currentTime;
        if (fileWriter != null) {
            if (!fileWriter.getFile().exists() || !fileWriter.getFile().getParentFile().exists()) {
                closeFileWriter();
                newLogFile(currentTime);
            }
            try {
                fileWriter.write(msg);
                fileWriter.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    protected static void writeRealMsg2File(long currentTime, String msg) {
        if (fileWriter != null) {
            try {
                fileWriter.write(msg);
                fileWriter.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    private static void newLogFile(long currentTime) {
        if (fileWriter == null) {
            currentLogDay = dataFormatter.format(new Date(currentTime));
            File tagFile = new File(mGlobalLogFilesPercent, "klog_" + currentLogDay + ".log");
            if (!tagFile.getParentFile().exists()) {
                tagFile.getParentFile().mkdirs();
            }
            boolean fileExists = tagFile.exists();
            if (!fileExists) {
                try {
                    tagFile.createNewFile();
                    zipHandler.sendEmptyMessageDelayed(1, 10 * 1000);
                } catch (Exception e) {
                    Log.e("KLog", "newLogFile 异常：" + ThrowableUtils.getFullStackTrace(e));
                    fileWriter = null;
                }
            }
            
            try {
                
                fileWriter = new AutoFileWriter(tagFile, true);
                if (!fileExists) {
                    fileWriter.write(KLog.LINE_SEPARATOR);
                    fileWriter.write(KLog.LINE_SEPARATOR);
                    fileWriter.write("klog_start:" + formatter.format(new Date(currentTime))
                            + "------------------------------------------------------------------");
                    fileWriter.write(KLog.LINE_SEPARATOR);
                }
            } catch (Exception e) {
                Log.e("KLog", "newLogFile 异常：" + ThrowableUtils.getFullStackTrace(e));
                fileWriter = null;
            }
            
            
        } else {
            String logDay = dataFormatter.format(new Date(currentTime));
            if (!currentLogDay.endsWith(logDay)) {
                if (fileWriter != null) {
                    try {
                        fileWriter.flush();
                        fileWriter.close();
                        fileWriter = null;
                    } catch (Exception e) {
                        try {
                            fileWriter.close();
                        } catch (Exception E) {
                            fileWriter = null;
                        }
                    }
                    newLogFile(currentTime);
                }
            }
        }
    }
    
    private static void closeFileWriter() {
        if (fileWriter != null) {
            try {
                fileWriter.flush();
                fileWriter.close();
                fileWriter = null;
            } catch (Exception e) {
                try {
                    fileWriter.close();
                } catch (Exception E) {
                    fileWriter = null;
                }
            }
        }
    }
    
    private static void clearTimeOutFile(long currentTime) {
        File tagFileParent = new File(mGlobalLogFilesPercent);
        if (tagFileParent.exists()) {
            for (File file : tagFileParent.listFiles()) {
                String fileName = file.getName();
                if (file.isFile() && logFileName2Time(fileName) != -1) {
                    try {
                        Date parse = dataFormatter.parse(fileName.split("_")[1].split("\\.")[0]);
                        if (currentTime - parse.getTime()
                                >= mGlobalHoldingDays * 24 * 60 * 60 * 1000) {
                            boolean delete = file.delete();
                            Log.d("KLog",
                                    "clear timeout delete file :" + file.getAbsolutePath() + ",d:"
                                            + delete);
                        }
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }
        } else {
            tagFileParent.mkdirs();
        }
    }
    
    private static synchronized void zipFile() {
        String currentLogDay = dataFormatter.format(new Date(getCurrentTime()));
        File tagFileParent = new File(mGlobalLogFilesPercent);
        if (tagFileParent.exists()) {
            File[] files = tagFileParent.listFiles(new FileFilter() {
                @Override
                public boolean accept(File file) {
                    return file.isFile() && logFileName2Time(file.getName()) != -1 && file.getName()
                            .endsWith("log");
                }
            });
            if (files == null || files.length == 0) {
                return;
            }
            List<File> fileList = Arrays.asList(files);
            Collections.sort(fileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    return (int) (logFileName2Time(o2.getName()) - logFileName2Time(o1.getName()));
                }
            });
            File currentDayFile = new File(mGlobalLogFilesPercent,
                    "klog_" + currentLogDay + ".log");
            for (File file : fileList) {
                if (!currentDayFile.getName().endsWith(file.getName())) {
                    File zipFile = new File(mGlobalLogFilesPercent,
                            file.getName().replace(".log", ".zip"));
                    boolean b = ZipUtils.zipFile(file.getAbsolutePath(), zipFile.getAbsolutePath());
                    if (b) {
                        boolean delete = file.delete();
                        Log.d("KLog", "zip success delete file :" + file.getAbsolutePath() + ",d:"
                                + delete);
                        
                    }
                }
            }
        } else {
            tagFileParent.mkdirs();
        }
    }
    
    private static void checkLogFileSize() {
        File tagFileParent = new File(mGlobalLogFilesPercent);
        if (tagFileParent.exists()) {
            File[] files = tagFileParent.listFiles(new FileFilter() {
                @Override
                public boolean accept(File file) {
                    return file.isFile() && logFileName2Time(file.getName()) != -1;
                }
            });
            if (files == null || files.length == 0) {
                return;
            }
            List<File> fileList = Arrays.asList(files);
            Collections.sort(fileList, new Comparator<File>() {
                @Override
                public int compare(File o1, File o2) {
                    return (int) (logFileName2Time(o2.getName()) - logFileName2Time(o1.getName()));
                }
            });
            long fileSizeLimit = mGlobalLogFilesSize;
            for (File file : fileList) {
                fileSizeLimit = fileSizeLimit - file.length();
                if (fileSizeLimit <= 0) {
                    boolean delete = file.delete();
                    Log.d("KLog",
                            "clear size delete file :" + file.getAbsolutePath() + ",d:" + delete);
                }
            }
        } else {
            tagFileParent.mkdirs();
        }
    }
    
    private static long logFileName2Time(String fileName) {
        Pattern pattern = Pattern.compile("klog_(\\d{4}-\\d{2}-\\d{2})\\.(log|zip)");
        Matcher matcher = pattern.matcher(fileName);
        
        if (matcher.find()) {
            String extractedDate = matcher.group(1);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            
            try {
                Date date = sdf.parse(extractedDate);
                long timestamp = date.getTime();
                return timestamp;
            } catch (Exception e) {
                Log.e("KLog", "logFileName2Time err:" + ThrowableUtils.getFullStackTrace(e));
            }
        }
        return -1;
    }
    
    
    public static void v() {
        printLog(false,V, null, DEFAULT_MESSAGE);
    }
    
    public static void v(Object msg) {
        printLog(false,V, null, msg);
    }
    
    public static void v(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,V, null, objects);
        } else {
            printLog(false,V, null, objects);
        }
    }
    
    public static void d() {
        printLog(false,D, null, DEFAULT_MESSAGE);
    }
    
    public static void d(Object msg) {
        printLog(false,D, null, msg);
    }
    
    public static void d(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,D, null, objects);
        } else {
            printLog(false,D, null, objects);
        }
    }
    
    public static void i() {
        printLog(false,I, null, DEFAULT_MESSAGE);
    }
    
    public static void i(Object msg) {
        printLog(false,I, null, msg);
    }
    
    public static void i(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,I, null, objects);
        } else {
            printLog(false,I, null, objects);
        }
    }
    
    public static void w() {
        printLog(false,W, null, DEFAULT_MESSAGE);
    }
    
    public static void w(Object msg) {
        printLog(false,W, null, msg);
    }
    
    public static void w(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,W, null, objects);
        } else {
            printLog(false,W, null, objects);
        }
    }
    
    public static void e() {
        printLog(false,E, null, DEFAULT_MESSAGE);
    }
    
    public static void e(Object msg) {
        printLog(false,E, null, msg);
    }
    
    public static void e(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,E, null, objects);
        } else {
            printLog(false,E, null, objects);
        }
    }
    
    public static void eRealLog(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(true, E, null, objects);
        } else {
            printLog(true, E, null, objects);
        }
    }
    
    public static void a() {
        printLog(false,A, null, DEFAULT_MESSAGE);
    }
    
    public static void a(Object msg) {
        printLog(false,A, null, msg);
    }
    
    public static void a(Object... objects) {
        if (objects == null || objects.length == 0) {
            printLog(false,A, null, objects);
        } else {
            printLog(false,A, null, objects);
        }
    }
    
    public static void json(String jsonFormat) {
        printLog(false,JSON, null, jsonFormat);
    }
    
    public static void xml(String xml) {
        printLog(false,XML, null, xml);
    }
    
    
    public static void file(File targetDirectory, Object msg) {
        printFile(null, targetDirectory, null, msg);
    }
    
    
    public static void file(File targetDirectory, String fileName, Object msg) {
        printFile(null, targetDirectory, fileName, msg);
    }
    
    public static void debug() {
        printDebug(null, DEFAULT_MESSAGE);
    }
    
    public static void debug(Object msg) {
        printDebug(null, msg);
    }
    
    public static void debug(Object... objects) {
        printDebug(null, objects);
    }
    
    public static void trace() {
        printStackTrace();
    }
    
    private static void printStackTrace() {
        
        if (!IS_SHOW_LOG) {
            return;
        }
        
        Throwable tr = new Throwable();
        StringWriter sw = new StringWriter();
        PrintWriter pw = new PrintWriter(sw);
        tr.printStackTrace(pw);
        pw.flush();
        String message = sw.toString();
        
        String traceString[] = message.split("\\n\\t");
        StringBuilder sb = new StringBuilder();
        sb.append("\n");
        for (String trace : traceString) {
            if (trace.contains("at com.socks.library.KLog")) {
                continue;
            }
            sb.append(trace).append("\n");
        }
        String[] contents = wrapperContent(STACK_TRACE_INDEX_4, null, sb.toString());
        String tag = contents[0];
        String msg = contents[1];
        String headString = contents[2];
        BaseLog.printDefault(false, E, tag, headString + msg);
    }
    
    private static void printLog(boolean isRealTime, int type, String tagStr, Object... objects) {
        String[] contents = wrapperContent(STACK_TRACE_INDEX_5, tagStr, objects);
        String tag = contents[0];
        String msg = contents[1];
        String headString = contents[2];
        
        switch (type) {
            case V:
            case D:
            case I:
            case W:
            case E:
            case A:
                BaseLog.printDefault(isRealTime, type, tag, headString + msg);
                break;
            case JSON:
                JsonLog.printJson(tag, msg, headString);
                break;
            case XML:
                XmlLog.printXml(tag, msg, headString);
                break;
        }
        if (onLogListener != null) {
            for (Object o : objects) {
                if (Objects.isNull(o)){
                    continue;
                }
                onLogListener.onLog(o.toString());
            }
            
        }
    }
    
    private static OnLogListener onLogListener = new OnLogListener() {
        @Override
        public void onLog(String log) {
        
        }
        
        @Override
        public long currentTime() {
            return System.currentTimeMillis();
        }
    };
    
    public static long getCurrentTime() {
        if (null != onLogListener) {
            return onLogListener.currentTime();
        }
        return System.currentTimeMillis();
    }
    
    public static void setOnLogListener(OnLogListener l) {
        onLogListener = l;
    }
    
    public interface OnLogListener {
        
        void onLog(String log);
        
        long currentTime();
    }
    
    private static void printDebug(String tagStr, Object... objects) {
        String[] contents = wrapperContent(STACK_TRACE_INDEX_5, tagStr, objects);
        String tag = contents[0];
        String msg = contents[1];
        String headString = contents[2];
        BaseLog.printDefault(false, D, tag, headString + msg);
    }
    
    
    private static void printFile(String tagStr, File targetDirectory, String fileName,
            Object objectMsg) {
        
        if (!IS_SHOW_LOG) {
            return;
        }
        
        String[] contents = wrapperContent(STACK_TRACE_INDEX_5, tagStr, objectMsg);
        String tag = contents[0];
        String msg = contents[1];
        String headString = contents[2];
        
        FileLog.printFile(tag, targetDirectory, fileName, headString, msg);
    }
    
    private static String[] wrapperContent(int stackTraceIndex, String tagStr, Object... objects) {
        
        StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
        
        StackTraceElement targetElement = stackTrace[stackTraceIndex];
        String className = targetElement.getClassName();
        String[] classNameInfo = className.split("\\.");
        if (classNameInfo.length > 0) {
            className = classNameInfo[classNameInfo.length - 1] + SUFFIX;
        }
        
        if (className.contains("$")) {
            className = className.split("\\$")[0] + SUFFIX;
        }
        
        String methodName = targetElement.getMethodName();
        int lineNumber = targetElement.getLineNumber();
        
        if (lineNumber < 0) {
            lineNumber = 0;
        }
        String tag = tagStr;
        
        if (null == tag) {
            if (!mIsGlobalTagEmpty) {
                tag = mGlobalTag;
            } else {
                tag = TAG_DEFAULT;
            }
        }
        
        String msg = (objects == null) ? NULL_TIPS : getObjectsString(objects);
        String headString = "[ (" + className + ":" + lineNumber + ")#" + methodName + " ] ";
        
        return new String[] {tag, msg, headString};
    }
    
    private static String getObjectsString(Object... objects) {
        
        if (objects.length > 1) {
            StringBuilder stringBuilder = new StringBuilder();
            for (int i = 0; i < objects.length; i++) {
                Object object = objects[i];
                if (object instanceof Throwable) {
                    object = ThrowableUtils.getFullStackTrace((Throwable) object);
                }
                if (object != null) {
                    stringBuilder.append(object);
                }
            }
            return stringBuilder.toString();
        } else {
            Object object = objects[0];
            if (object instanceof Throwable) {
                object = ThrowableUtils.getFullStackTrace((Throwable) object);
            }
            return object == null ? NULL : object.toString();
        }
    }
    
}
